/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.csp.sentinel.slots.block.flow.controller;

import java.util.concurrent.atomic.AtomicLong;

import com.alibaba.csp.sentinel.config.SentinelConfig;
import com.alibaba.csp.sentinel.util.TimeUtil;
import com.alibaba.csp.sentinel.node.Node;
import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;

/**
 * <p>
 * The principle idea comes from Guava. However, the calculation of Guava is
 * rate-based, which means that we need to translate rate to QPS.
 * </p>
 *
 * <p>
 * Requests arriving at the pulse may drag down long idle systems even though it
 * has a much larger handling capability in stable period. It usually happens in
 * scenarios that require extra time for initialization, e.g. DB establishes a connection,
 * connects to a remote service, and so on. That’s why we need “warm up”.
 * </p>
 *
 * <p>
 * Sentinel's "warm-up" implementation is based on the Guava's algorithm.
 * However, Guava’s implementation focuses on adjusting the request interval,
 * which is similar to leaky bucket. Sentinel pays more attention to
 * controlling the count of incoming requests per second without calculating its interval,
 * which resembles token bucket algorithm.
 * </p>
 *
 * <p>
 * The remaining tokens in the bucket is used to measure the system utility.
 * Suppose a system can handle b requests per second. Every second b tokens will
 * be added into the bucket until the bucket is full. And when system processes
 * a request, it takes a token from the bucket. The more tokens left in the
 * bucket, the lower the utilization of the system; when the token in the token
 * bucket is above a certain threshold, we call it in a "saturation" state.
 * </p>
 *
 * <p>
 * Base on Guava’s theory, there is a linear equation we can write this in the
 * form y = m * x + b where y (a.k.a y(x)), or qps(q)), is our expected QPS
 * given a saturated period (e.g. 3 minutes in), m is the rate of change from
 * our cold (minimum) rate to our stable (maximum) rate, x (or q) is the
 * occupied token.
 * </p>
 *
 * @author jialiang.linjl
 */
public class WarmUpController implements TrafficShapingController {

    // 阈值
    protected double count;
    /**
     * 冷启动的因子 ，默认为3 {@link SentinelConfig#coldFactor()}
     */
    private int coldFactor;
    // 转折点的令牌数
    protected int warningToken = 0;
    // 最大令牌数
    private int maxToken;
    // 折线初始斜率，标志流量的变化程度
    protected double slope;

    // 累积的令牌数 ,累积的令牌数越多，说明系统利用率越低，说明当前流量低，是冷状态
    protected AtomicLong storedTokens = new AtomicLong(0);
    // 最后更新令牌的时间
    protected AtomicLong lastFilledTime = new AtomicLong(0);

    public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
        construct(count, warmUpPeriodInSec, coldFactor);
    }

    public WarmUpController(double count, int warmUpPeriodInSec) {
        construct(count, warmUpPeriodInSec, 3);
    }

    /**
     * @param count             用户设定的阈值（这里假设设定为100）
     * @param warmUpPeriodInSec 默认为10
     * @param coldFactor        默认为3
     */
    private void construct(double count, int warmUpPeriodInSec, int coldFactor) {

        if (coldFactor <= 1) {
            throw new IllegalArgumentException("Cold factor should be larger than 1");
        }

        this.count = count;

        this.coldFactor = coldFactor;

        // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
        // warningToken = 100;

        // 按默认的warmUpPeriodInSec = 10，表示1秒钟10个请求，则每个请求为间隔stableInterval = 100ms，那么coldInterval=stableInterval * coldFactor = 100 * 3 = 300ms
        // warningToken = 10 * 100 / (3 - 1) = 500
        // thresholdPermits = warningToken = 0.5 * warmupPeriod / stableInterval = 0.5 * warmupPeriod / 100ms = 500 ==>> warmupPeriod = 100000ms
        warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);

        // / maxPermits = thresholdPermits + 2 * warmupPeriod /
        // (stableInterval + coldInterval)
        // maxToken = 200

        // maxPermits = 500 + 2 * 100000ms / (100ms + 300ms) = 1000
        // maxToken = 500 + (2 * 10 * 100 / (1.0 + 3)) = 1000
        // maxPermits = maxToken
        maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));

        // slope
        // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits
        // - thresholdPermits);

        // slope = (3 - 1.0) / 100 / (600 - 500) = 0.0002
        slope = (coldFactor - 1.0) / count / (maxToken - warningToken);

    }

    @Override
    public boolean canPass(Node node, int acquireCount) {
        return canPass(node, acquireCount, false);
    }

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        // 当前已经通过的qps
        long passQps = (long) node.passQps();

        // 上一个滑动窗口的qps
        long previousQps = (long) node.previousPassQps();
        // 同步令牌，如果是出于冷启动或预热完毕状态，则考虑要添加令牌
        syncToken(previousQps);

        // 开始计算它的斜率
        // 如果进入了警戒线，开始调整他的qps
        long restToken = storedTokens.get();
        if (restToken >= warningToken) {
            // 计算当前离警戒线的距离
            long aboveToken = restToken - warningToken;
            // 消耗的速度要比warning快，但是要比慢
            // current interval = restToken*slope+1/count
            // restToken越小，interval就越小，表示系统越热
            // 随着aboveToken的减小，warningQps会逐渐增大
            double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
            if (passQps + acquireCount <= warningQps) { // 随着warningQps的增大，acquireCount = 1，那么passQps允许的范围就变大，相应的流量就越大，系统越热
                return true;
            }
        } else {
            if (passQps + acquireCount <= count) {
                return true;
            }
        }

        return false;
    }

    /**
     * 同步令牌
     * @param passQps
     */
    protected void syncToken(long passQps) {
        long currentTime = TimeUtil.currentTimeMillis();
        // 把当前时间的后三位置为0 e.g. 1601456312835 = 1601456312835 - 1601456312835 % 1000 = 1601456312000
        currentTime = currentTime - currentTime % 1000;
        // 获取上一次更新令牌的时间
        long oldLastFillTime = lastFilledTime.get();
        if (currentTime <= oldLastFillTime) {
            return;
        }

        // 获得目前的令牌数
        long oldValue = storedTokens.get();
        // 获取新的令牌数
        long newValue = coolDownTokens(currentTime, passQps);

        // 更新累积令牌数
        if (storedTokens.compareAndSet(oldValue, newValue)) {
            // 去除上一次的qps，设置剩下的令牌数
            long currentValue = storedTokens.addAndGet(0 - passQps);
            if (currentValue < 0) {
                // 如果剩下的令牌数小于0，则置为0。
                storedTokens.set(0L);
            }
            // 设置令牌更新时间
            lastFilledTime.set(currentTime);
        }
    }

    private long coolDownTokens(long currentTime, long passQps) {
        // 当前拥有的令牌数
        long oldValue = storedTokens.get();
        long newValue = oldValue;

        // 添加令牌的判断前提条件:
        // 当令牌的消耗程度远远低于警戒线的时候
        if (oldValue < warningToken) { // 这种情况表示已经预热结束，可以开始生成令牌了
            // 这里按照count = 100来计算的话，表示旧值oldValue + 距离上次更新的秒数时间差 * count ，表示每秒增加count个令牌
            // 这里的currentTime 和 lastFilledTime.get() 都是已经去掉毫秒数的
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        } else if (oldValue > warningToken) { // 进入这里表示当前是冷状态或正处于预热状态
            if (passQps < (int)count / coldFactor) { // 如果是冷状态，则补充令牌数，避免令牌数为0
                newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
            }
            // 预热阶段则不添加令牌数，从而限制流量的急剧攀升
        }
        // 限制令牌数不能超过最大令牌数maxToken
        return Math.min(newValue, maxToken);
    }


    public static void main(String[] args) {
        System.out.println(TimeUtil.currentTimeMillis());
        long a = 1601456312835l - 1601456312835l % 1000;
        System.out.println(a);
    }
}
